| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- // @ts-nocheck
- import { useParams } from 'common'
- import { ChangeEvent, useEffect, useRef, useState } from 'react'
- import { AWS_REGIONS } from 'shared-data'
- import { toast } from 'sonner'
- import {
- Button,
- Checkbox,
- Input,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- } from 'ui'
- import { Admonition } from 'ui-patterns/admonition'
- import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import { isVercelUrl } from '@/components/interfaces/Integrations/Vercel/VercelIntegration.utils'
- import { Markdown } from '@/components/interfaces/Markdown'
- import VercelIntegrationWindowLayout from '@/components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout'
- import { ScaffoldColumn, ScaffoldContainer } from '@/components/layouts/Scaffold'
- import { PasswordStrengthBar } from '@/components/ui/PasswordStrengthBar'
- import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
- import { useIntegrationsQuery } from '@/data/integrations/integrations-query'
- import { useIntegrationVercelConnectionsCreateMutation } from '@/data/integrations/integrations-vercel-connections-create-mutation'
- import { useVercelProjectsQuery } from '@/data/integrations/integrations-vercel-projects-query'
- import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
- import { useProjectCreateMutation } from '@/data/projects/project-create-mutation'
- import {
- useDataApiRevokeOnCreateDefaultEnabled,
- useTrackDefaultPrivilegesExposure,
- } from '@/hooks/misc/useDataApiRevokeOnCreateDefault'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- import { usePHFlag } from '@/hooks/ui/useFlag'
- import { BASE_PATH, PROVIDERS } from '@/lib/constants'
- import { getInitialMigrationSQLFromGitHubRepo } from '@/lib/integration-utils'
- import { passwordStrength, PasswordStrengthScore } from '@/lib/password-strength'
- import { generateStrongPassword } from '@/lib/project'
- import { useTrack } from '@/lib/telemetry/track'
- import { useIntegrationInstallationSnapshot } from '@/state/integration-installation'
- import type { NextPageWithLayout } from '@/types'
- const VercelIntegration: NextPageWithLayout = () => {
- return (
- <>
- <ScaffoldContainer className="flex flex-col gap-6 grow py-8">
- <ScaffoldColumn className="mx-auto w-full max-w-md">
- <header>
- <h2>New project</h2>
- <Markdown
- className="text-foreground-light"
- content={`Choose the Briven organization you wish to install in`}
- />
- </header>
- <CreateProject />
- <Admonition
- type="default"
- layout="horizontal"
- title="You can uninstall this Integration at any time."
- description="You can remove this integration at any time via Vercel or the Briven dashboard"
- />
- </ScaffoldColumn>
- </ScaffoldContainer>
- </>
- )
- }
- VercelIntegration.getLayout = (page) => (
- <VercelIntegrationWindowLayout>{page}</VercelIntegrationWindowLayout>
- )
- const CreateProject = () => {
- const { data: selectedOrganization } = useSelectedOrganizationQuery()
- const [projectName, setProjectName] = useState('')
- const [dbPass, setDbPass] = useState('')
- const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('')
- const [passwordStrengthScore, setPasswordStrengthScore] = useState(-1)
- const [shouldRunMigrations, setShouldRunMigrations] = useState(true)
- const [dbRegion, setDbRegion] = useState<string>(PROVIDERS.AWS.default_region.displayName)
- const track = useTrack()
- const snapshot = useIntegrationInstallationSnapshot()
- const isDataApiRevokeOnCreateDefault = useDataApiRevokeOnCreateDefaultEnabled()
- const dataApiRevokeOnCreateDefaultFlag = usePHFlag<boolean>('dataApiRevokeOnCreateDefault')
- const [dataApiDefaultPrivileges, setDataApiDefaultPrivileges] = useState(
- !isDataApiRevokeOnCreateDefault
- )
- const hasUserModifiedDataApiDefaultPrivileges = useRef(false)
- useEffect(() => {
- if (dataApiRevokeOnCreateDefaultFlag === undefined) return
- if (hasUserModifiedDataApiDefaultPrivileges.current) return
- setDataApiDefaultPrivileges(!dataApiRevokeOnCreateDefaultFlag)
- }, [dataApiRevokeOnCreateDefaultFlag])
- const { slug, next, currentProjectId: foreignProjectId, externalId } = useParams()
- useTrackDefaultPrivilegesExposure({
- surface: 'vercel',
- orgSlug: slug,
- dataApiDefaultPrivileges,
- hasUserModified: hasUserModifiedDataApiDefaultPrivileges.current,
- })
- async function checkPasswordStrength(value: string) {
- const { message, strength } = await passwordStrength(value)
- setPasswordStrengthScore(strength)
- setPasswordStrengthMessage(message)
- }
- const { mutateAsync: createConnections } = useIntegrationVercelConnectionsCreateMutation()
- const { data: organizationData } = useOrganizationsQuery()
- const organization = organizationData?.find((x) => x.slug === slug)
- /**
- * array of integrations installed
- */
- const { data: integrationData } = useIntegrationsQuery()
- /**
- * the vercel integration installed for organization chosen
- */
- const organizationIntegration = integrationData?.find((x) => x.organization.slug === slug)
- /**
- * Vercel projects available for this integration
- */
- const { data: vercelProjects } = useVercelProjectsQuery(
- {
- organization_integration_id: organizationIntegration?.id,
- },
- { enabled: organizationIntegration !== undefined }
- )
- function onProjectNameChange(e: ChangeEvent<HTMLInputElement>) {
- e.target.value = e.target.value.replace(/\./g, '')
- setProjectName(e.target.value)
- }
- function onDbPassChange(e: ChangeEvent<HTMLInputElement>) {
- const value = e.target.value
- setDbPass(value)
- if (value == '') {
- setPasswordStrengthScore(-1)
- setPasswordStrengthMessage('')
- } else checkPasswordStrength(value)
- }
- function generatePassword() {
- const password = generateStrongPassword()
- setDbPass(password)
- checkPasswordStrength(password)
- }
- const [newProjectRef, setNewProjectRef] = useState<string | undefined>(undefined)
- const { mutate: createProject } = useProjectCreateMutation({
- onSuccess: (res) => {
- setNewProjectRef(res.ref)
- track(
- 'project_creation_simple_version_submitted',
- {
- surface: 'vercel',
- dataApiEnabled: true,
- dataApiDefaultPrivilegesGranted: dataApiDefaultPrivileges,
- ...(dataApiRevokeOnCreateDefaultFlag !== undefined && {
- dataApiRevokeOnCreateDefaultEnabled: dataApiRevokeOnCreateDefaultFlag,
- }),
- },
- {
- project: res.ref,
- organization: res.organization_slug,
- }
- )
- },
- onError: (error) => {
- toast.error(error.message)
- snapshot.setLoading(false)
- },
- })
- async function onCreateProject() {
- if (!organizationIntegration) return console.error('No organization installation details found')
- if (!organizationIntegration?.id) return console.error('No organization installation ID found')
- if (!foreignProjectId) return console.error('No foreignProjectId set')
- if (!organization) return console.error('No organization set')
- snapshot.setLoading(true)
- let dbSql: string | undefined
- if (shouldRunMigrations) {
- const id = toast(`Fetching initial migrations from GitHub repo`)
- const migrationSql = await getInitialMigrationSQLFromGitHubRepo(externalId)
- if (migrationSql) dbSql = migrationSql
- toast.success(`Done fetching initial migrations`, { id })
- }
- createProject({
- organizationSlug: organization.slug,
- name: projectName,
- dbPass,
- dbRegion,
- dbSql,
- dataApiRevokeDefaultPrivileges: !dataApiDefaultPrivileges,
- })
- }
- // Wait for the new project to be created before creating the connection
- const { data, isSuccess } = useProjectSettingsV2Query(
- { projectRef: newProjectRef },
- {
- enabled: newProjectRef !== undefined,
- // refetch until the project is created
- refetchInterval: (query) => {
- const data = query.state.data
- return ((data?.service_api_keys ?? []).length ?? 0) > 0 ? false : 1000
- },
- }
- )
- useEffect(() => {
- if (!isSuccess) return
- const onSuccessFunc = async () => {
- const isReady = (data.service_api_keys ?? []).length > 0
- if (!isReady || !organizationIntegration || !foreignProjectId || !newProjectRef) {
- return
- }
- const projectDetails = vercelProjects?.find((x: any) => x.id === foreignProjectId)
- try {
- await createConnections({
- organizationIntegrationId: organizationIntegration?.id,
- connection: {
- foreign_project_id: foreignProjectId,
- briven_project_ref: newProjectRef,
- integration_id: '0',
- metadata: {
- ...projectDetails,
- brivenConfig: {
- projectEnvVars: {
- write: true,
- },
- },
- },
- },
- orgSlug: selectedOrganization?.slug,
- })
- } catch (error) {
- console.error('An error occurred during createConnections:', error)
- return
- }
- snapshot.setLoading(false)
- if (next && isVercelUrl(next)) {
- window.location.href = next
- }
- }
- onSuccessFunc()
- }, [data, isSuccess])
- return (
- <div>
- <p className="mb-2">Briven project details</p>
- <div className="py-2">
- <FormItemLayout
- id="projectName"
- isReactForm={false}
- layout="vertical"
- label="Project name"
- size="tiny"
- >
- <Input
- autoFocus
- id="projectName"
- type="text"
- placeholder=""
- value={projectName}
- onChange={onProjectNameChange}
- />
- </FormItemLayout>
- </div>
- <div className="py-2">
- <FormItemLayout
- id="dbPass"
- isReactForm={false}
- layout="vertical"
- label="Database password"
- size="tiny"
- description={
- <PasswordStrengthBar
- passwordStrengthScore={passwordStrengthScore as PasswordStrengthScore}
- password={dbPass}
- passwordStrengthMessage={passwordStrengthMessage}
- generateStrongPassword={generatePassword}
- />
- }
- >
- <PasswordInput
- id="dbPass"
- type="password"
- placeholder="Type in a strong password"
- value={dbPass}
- reveal
- copy={dbPass.length > 0}
- onChange={onDbPassChange}
- />
- </FormItemLayout>
- </div>
- <div className="py-2">
- <div className="mt-1">
- <FormItemLayout
- id="region"
- isReactForm={false}
- layout="vertical"
- label="Region"
- description="Select a region close to your users for the best performance."
- className="gap-[2px]"
- size="tiny"
- >
- <Select value={dbRegion} onValueChange={(region) => setDbRegion(region)}>
- <SelectTrigger id="region">
- <SelectValue />
- </SelectTrigger>
- <SelectContent>
- {Object.keys(AWS_REGIONS).map((option: string, i) => {
- const label = Object.values(AWS_REGIONS)[i].displayName
- return (
- <SelectItem key={option} value={label}>
- <div className="flex gap-2">
- <img
- alt="region icon"
- className="w-5 rounded-xs"
- src={`${BASE_PATH}/img/regions/${Object.values(AWS_REGIONS)[i].code}.svg`}
- />
- <span>{label}</span>
- </div>
- </SelectItem>
- )
- })}
- </SelectContent>
- </Select>
- </FormItemLayout>
- </div>
- </div>
- <div className="py-2 pb-4">
- <div className="items-top flex space-x-2">
- <Checkbox
- id="shouldRunMigrations"
- name="shouldRunMigrations"
- checked={shouldRunMigrations}
- onCheckedChange={(checked) => setShouldRunMigrations(!!checked)}
- />
- <div className="grid gap-1.5 leading-none">
- <label
- htmlFor="enable-realtime"
- className="text-sm text-foreground-light flex items-center space-x-2 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
- >
- Create sample tables with seed data
- </label>
- <p className="text-sm text-foreground-muted">
- To get you started quickly, we can create new tables for you with seed (sample) data.
- You can delete these tables later.
- </p>
- </div>
- </div>
- </div>
- <div className="py-2 pb-4">
- <div className="items-top flex space-x-2">
- <Checkbox
- id="dataApiDefaultPrivileges"
- name="dataApiDefaultPrivileges"
- checked={dataApiDefaultPrivileges}
- onCheckedChange={(checked) => {
- hasUserModifiedDataApiDefaultPrivileges.current = true
- setDataApiDefaultPrivileges(!!checked)
- }}
- />
- <div className="grid gap-1.5 leading-none">
- <label
- htmlFor="dataApiDefaultPrivileges"
- className="text-sm text-foreground-light flex items-center space-x-2 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
- >
- Automatically expose new tables
- </label>
- <p className="text-sm text-foreground-muted">
- Grants privileges to Data API roles by default, exposing new tables. We recommend
- disabling this to control access manually.
- </p>
- </div>
- </div>
- </div>
- <div className="flex flex-row w-full justify-end">
- <Button
- size="medium"
- className="self-end"
- disabled={snapshot.loading}
- loading={snapshot.loading}
- onClick={onCreateProject}
- >
- Create Project
- </Button>
- </div>
- </div>
- )
- }
- export default VercelIntegration
|